package ar.edu.itba.it.pdc.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;

import org.apache.log4j.Logger;

import ar.edu.itba.it.pdc.httpParser.exceptions.HttpBadRequestException;
import ar.edu.itba.it.pdc.httpParser.exceptions.HttpMethodNotSuportedException;
import ar.edu.itba.it.pdc.httpParser.exceptions.HttpVersionNotSuportedException;
import ar.edu.itba.it.pdc.httpParser.models.HttpRequest;
import ar.edu.itba.it.pdc.httpParser.models.HttpResponse;
import ar.edu.itba.it.pdc.httpParser.models.HttpStatusLine;
import ar.edu.itba.it.pdc.monitoring.ProxyMonitoring;
import ar.edu.itba.it.pdc.nio.Connection.ConectionType;

public class ProxySelectorProtocol implements TCPProtocol {

	private static final int RETRIESNUM = 2;
	private HashMap<SelectionKey, Connection> map;
	private Logger logger = Logger.getLogger(ProxySelectorProtocol.class);
	private Logger accessLogger = Logger.getLogger("accessLogger");
	
	public ProxySelectorProtocol() throws IOException {
		map = new HashMap<SelectionKey, Connection>();
	}

	@Override
	public void handleAccept(SelectionKey key) throws IOException {
		SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
		clntChan.configureBlocking(false); // Must be nonblocking to register
		// Register the selector with new channel for read and attach byte
		// buffer
		clntChan.register(key.selector(), SelectionKey.OP_READ, null);
		ProxyMonitoring.getInstance().openConnection();
	}

	@Override
	public void handleRead(SelectionKey key) throws IOException {
		// try{
		// Client socket channel has pending data
		SocketChannel clntChan = (SocketChannel) key.channel();
		if (map.get(key) == null) {
			Connection req = new Connection(key);
			map.put(key, req);
		}

		UnidirectionalConnection conection;
		if (key == map.get(key).getRequest().getKey()) {
			conection = map.get(key).getRequest();
		} else if (key == map.get(key).getResponse().getKey()) {
			conection = map.get(key).getResponse();
		} else {
			return;
		}
		long bytesRead;
		try {
			bytesRead = conection.read(clntChan);
			if (bytesRead == -1) { // Did the other end close?

				conection.close();
				clntChan.close();
			} else if (bytesRead >= 0) {
				key.interestOps(SelectionKey.OP_READ);
			}
		} catch (MemoryAllocationLimitHited e) {
			conection.close();
			
			clntChan.close();
			Connection con = map.get(key);
			con.retriveBuffers();
			map.remove(con.getRequest().getKey());
			return;
			
		} catch (IOException e) {
			conection.close();
			clntChan.close();
			Connection con = map.get(key);
			con.retriveBuffers();
			map.remove(con.getRequest().getKey());
			return;

		}

		try {
			if (conection.process()) {

				if (!map.get(key).isEntablished()) {
					
					if(conection.filterFailure()){
						logger.debug("Request filtered. Returning 403 Forbidden.");
						manageError(key, "403","Forbidden");
						return;
					}
					
					if (conection.getIp() == null) {
						throw new UnknownHostException();
					}
					logger.debug("Establishing connection with host: " + map.get(key).getRequest().getModel().getHeaderValue("Host"));
					SocketChannel exitSocket = SocketChannel.open(new InetSocketAddress(conection.getIp(), conection.getPort()));
					exitSocket.configureBlocking(false);
					SelectionKey exitKey = exitSocket.register(key.selector(), SelectionKey.OP_READ, null);
					map.get(key).setResponseKey(exitKey);
					map.put(exitKey, map.get(key));
					logRequest(key);
					
				}

				if (map.get(key).getResponse() != null && map.get(key).getResponse().getKey() == key) {
					map.get(key).setEntablished(false);
					logResponse(key);
				}

				if (conection.getWKey().isValid()) {
					conection.getWKey().interestOps(SelectionKey.OP_WRITE);
				}
			}
		} catch (HttpMethodNotSuportedException e) {
			logger.warn("Returning 405 Method Not Allowed.");
			manageError(key, "405", "Method Not Allowed");
		} catch (HttpBadRequestException e) {
			logger.warn("Returning 400 Bad Request.");
			manageError(key, "400", "Bad Request");
		} catch (HttpVersionNotSuportedException e) {
			logger.warn("Returning 505 HTTP Version not suported.");
			manageError(key, "505", "HTTP Version not suported");
		} catch (UnknownHostException e) {
			logger.warn("Returning 502 Bad Gateway.");
			manageError(key, "502", "Bad Gateway");
		} catch (MemoryAllocationLimitHited e) {
			conection.close();
			clntChan.close();
			Connection con = map.get(key);
			con.retriveBuffers();
			map.remove(con.getRequest().getKey());
		} catch(Exception e) { 
			
			if(key.isValid()){
				manageError(key, "500", "Internal Server Error");
			} else { 
				Connection con = map.get(key); map.remove(con.getRequest().getKey());
			}
		}
	}

	private void manageError(SelectionKey key, String code, String desc) {
			try {
				if(map.get(key).isEntablished()){
					if (key.isValid()) {
						logger.warn("Returning 500 Internal Server Error.");
						map.get(key).returnError("500","Internal Server Error");
						map.get(key).getResponse().getWKey().interestOps(SelectionKey.OP_WRITE);
					} else {
						Connection con = map.get(key);
						con.retriveBuffers();
						map.remove(con.getRequest().getKey());
						map.remove(con.getRequest().getWKey());

					}
				} else {
					if (key.isValid()) {
						map.get(key).returnError(code, desc);
						key.interestOps(SelectionKey.OP_WRITE);
					} else {
						Connection con = map.get(key);
						con.retriveBuffers();
						map.remove(con.getRequest().getKey());
	
					}
				}
			} catch (MemoryAllocationLimitHited e) {
				Connection con = map.get(key);
				con.retriveBuffers();
				map.remove(con.getRequest().getKey());
			}
	}

	@Override
	public void handleWrite(SelectionKey key) throws IOException {
		/*
		 * Channel is available for writing, and key is valid (i.e., client
		 * channel not closed).
		 */
		// Retrieve data read earlier
		UnidirectionalConnection conection;
		if (key == map.get(key).getRequest().getKey()) {
			conection = map.get(key).getResponse();
		} else if (key == map.get(key).getResponse().getKey()) {
			conection = map.get(key).getRequest();
		} else {
			return;
		}

		SocketChannel clntChan = (SocketChannel) key.channel();
		try {
			conection.write(clntChan);
		} catch (IOException e) {
			
			if (conection.getConnectionType() == ConectionType.REQUEST) {
				SelectionKey newKey;
				if ((newKey = handleRetry(conection)) != null) {
					newKey.interestOps(SelectionKey.OP_WRITE);
				} else {
					logger.warn("Returning 500 Internal Server Error.");
					manageError(conection.getKey(), "500", "Internal Server Error");
				}
				
			} else {
				handleClose(key);
				return;
			}			
		}
		
		if (!conection.hasRemaining()) { // Buffer completely written?
			// Nothing left, so no longer interested in writes
			
			key.interestOps(SelectionKey.OP_READ);
			if (conection.isClosed()) {
				logger.debug("Closing client socket.");
				handleClose(key);				
			}
		}
	}

	private SelectionKey handleRetry(UnidirectionalConnection conection) {

		int i;
		for(i = 0; i < RETRIESNUM; i++) {
			SelectionKey key;
			if ((key = retry(conection)) != null) {
				return key;
			}
		}
		return null;
	}

	private void handleClose(SelectionKey key) throws IOException {
		key.channel().close();
		ProxyMonitoring.getInstance().closeConnection();
		
		Connection con = map.get(key);
		con.retriveBuffers();
		map.remove(con.getRequest().getKey());
		map.remove(con.getResponse().getKey());		
	}
	
	private SelectionKey retry(UnidirectionalConnection conection) {
		
		try {
			logger.debug("Retrying to establish connection with origin server.");
			SelectionKey key = conection.getKey();
			SocketChannel exitSocket = SocketChannel.open(new InetSocketAddress(conection.getIp(), 80));
			exitSocket.configureBlocking(false);
			SelectionKey exitKey = exitSocket.register(key.selector(), SelectionKey.OP_READ, null);
			map.get(key).setResponseKey(exitKey);
			map.put(exitKey, map.get(key));
			return exitKey;
		} catch (IOException e) {
			return null;
		}
	}
	
	private void logResponse(SelectionKey key) {
		HttpRequest req = (HttpRequest) map.get(key).getRequest().getModel();
		HttpResponse resp = (HttpResponse) map.get(key).getResponse().getModel();
		int status = ((HttpStatusLine)resp.getInitialLine()).getStatusCode();
		String desc = ((HttpStatusLine)resp.getInitialLine()).getStatusDesc();
		accessLogger.info("Serving request for URL: " + req.getURI() + " with a status code " + status + " " + desc);
	}

	private void logRequest(SelectionKey key) {
		HttpRequest req = (HttpRequest) map.get(key).getRequest().getModel();
		accessLogger.info("Sending request for URL: " + req.getURI() + " to host " + req.getHeaderValue("Host") );
	}
	
}
